------------The Quarter Mile-----------
A 4am crack                  2020-01-09
---------------------------------------

Name: The Quarter Mile
Version: 4.0
Genre: educational
Year: 1992
Publisher: Barnum Software
Platform: Apple ][ with 3.5-inch drive
Media: 3.5-inch disk
Disks: 1
OS: ProDOS 1.7
Previous cracks: none
Similar cracks:
  #2032 Troll Sports Math

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


Copy ][+ 9.1 ("COPY" > "DISK")
  no read errors, but copy loads ProDOS
  then crashes at $C505

CFFA 3000 import
  no read errors, but booting the disk
  image in an emulator exhibits the
  same behavior as the backup I made
  on real hardware with Copy ][+

I do not know if the original disk
boots. The slim amount of documentation
I received with the disk states that it
requires a IIgs, //c+, or a //e with a
UniDisk 3.5 drive, which I do not have.
(I have an Apple //e with a SuperDrive
in slot 2, which is admittedly pushing
it since even some unprotected 3.5-inch
disks require booting from slot 5.)

However, legitimate disk read failures
do not tend to crash, so I'm guessing
this is the failure mode of a run-time
protection check.

Next steps:

  1. Trace the startup program
  2. Find and disable the protection
     check
  3. Declare victory(*)

(*) go to the gym

                   ~

               Chapter 1
   In Which Things Quickly Get Hairy


The disk presents as standard ProDOS,
with a readable disk catalog.

]CAT,S2,D2

]CATALO\
]CAT,S7,d2

/QUARTER.MILE

 NAME           TYPE  BLOCKS  MODIFIED

 PRODOS          SYS      32  22-MAR-89
 COM.SYSTEM      SYS       8   1-JUN-92
 TP              BIN      17  15-APR-92
 PROGA           BIN      32  29-APR-92
 P               BIN      38  28-APR-92
 KEYBOARDING.    DIR       1   2-NOV-96
 WHOLE.NUMBERS.  DIR       2   2-NOV-96
 FRACTION.INTRO. DIR       1   5-NOV-96
 FRACTIONS.      DIR       1  <NO DATE>
 DECIMALS.       DIR       1   1-JUN-92
 PERCENTS.       DIR       1  <NO DATE>
 INTEGERS.       DIR       1  <NO DATE>
 EQUATIONS.      DIR       1  21-SEP-93

BLOCKS FREE:  913     BLOCKS USED:  687

]PREFIX /QUARTER.MILE
]BLOAD COM.SYSTEM,A$2000,TSYS
]CALL-151

*2000L

2000-   6C 7A 2C    JMP   ($2C7A)

*2C7A.2C7B

2C7A- 24 2C

*2C24L

; save flags
2C24-   08          PHP
2C25-   D8          CLD

; check for Apple IIgs
2C26-   38          SEC
2C27-   20 1F FE    JSR   $FE1F
2C2A-   B0 02       BCS   $2C2E

; IIgs-specific instruction (SEP #$30)
; which forces some bits in the status
; register to 8-bit mode
2C2C-  [E2 30]

; lightly obfuscated code here, which
; I've taken the liberty of lightly
; de-obfuscating
2C2E-   A0 00       LDY   #$00
2C30-   84 06       STY   $06
2C32-   F0 01       BEQ   $2C35
2C34-  [AF]
2C35-   A0 00       LDY   #$00
2C37-   A9 20       LDA   #$20
2C39-   D0 01       BNE   $2C3C
2C3B-  [5C]
2C3C-   85 07       STA   $07
2C3E-   D0 01       BNE   $2C41
2C40-  [22]
2C41-   20 74 2C    JSR   $2C74

*2C74L

; get a byte and "decrypt" (XOR) it
2C74-   B1 06       LDA   ($06),Y
2C76-   49 FC       EOR   #$FC
2C78-   38          SEC
2C79-   60          RTS

; always branches
2C44-   B0 01       BCS   $2C47
2C46-  [43]

; store decrypted byte in place
2C47-   91 06       STA   ($06),Y
2C49-   C8          INY
2C4A-   D0 F5       BNE   $2C41

; always branches
2C4C-   F0 01       BEQ   $2C4F
2C4E-  [C7]

; decrypt more pages
2C4F-   E6 07       INC   $07
2C51-   D0 01       BNE   $2C54
2C53-  [22]

; until $2C00
2C54-   A5 07       LDA   $07
2C56-   C9 2C       CMP   #$2C
2C58-   D0 E7       BNE   $2C41
2C5A-   F0 01       BEQ   $2C5D
2C5C-  [13]

; even more
2C5D-   20 74 2C    JSR   $2C74
2C60-   B0 01       BCS   $2C63
2C62-  [27]
2C63-   91 06       STA   ($06),Y
2C65-   C8          INY

; until $2C23
2C66-   C0 23       CPY   #$23
2C68-   D0 F3       BNE   $2C5D

; always branches
2C6A-   F0 01       BEQ   $2C6D
2C6C-  [3C]

; execute continues here
2C6D-   6C 71 2C    JMP   ($2C71)

That was a lot of work to decrypt what
I can only assume is the protection
routine.

*2C71.2C72

2C71- 00 28

That's in the code we just decrypted,
so let's do that.

; RTS instead of JMP
*2C6D:60

; execute the decryption loops (without
; the initial PHP)
*2C2EG

Piece of cake.

                   ~

               Chapter 2
  In Which It Is Most Definitely Not
 A Piece Of Cake And The Author Would
    Appreciate It If He Would Stop
            Calling It That


Let's see what wonderous code awaits us
after all that obfuscation, decryption,
and indirection.

*2800L

; mangle the reset vector, so you know
; this is getting esrious
2800-   A0 10       LDY   #$10
2802-   8C F4 03    STY   $03F4

; hard-coded to assume we're booting
; from slot 5, and self-modify some
; code later
2805-   AD FF C5    LDA   $C5FF
2808-   18          CLC
2809-   69 03       ADC   #$03
280B-   8D 9A 28    STA   $289A

; take ProDOS boot slot/drive and
; store it
280E-   AD 30 BF    LDA   $BF30
2811-   8D 5F 29    STA   $295F

; check high bit of last-accessed drive
; (0 = drive 1, 1 = drive 2)
2814-   29 80       AND   #$80
2816-   0A          ASL
2817-   90 17       BCC   $2830

; this protection check supports
; launching from drive 2 (but again,
; only from slot 5, because f--- you)
2819-   A9 02       LDA   #$02
281B-   8D 58 29    STA   $2958
281E-   8D 65 29    STA   $2965
2821-   8D 6C 29    STA   $296C
2824-   8D 77 29    STA   $2977
2827-   8D 7F 29    STA   $297F
282A-   8D 84 29    STA   $2984
282D-   8D 90 29    STA   $2990

; save page 3 vectors
2830-   A0 00       LDY   #$00
2832-   B9 D0 03    LDA   $03D0,Y
2835-   99 F2 2B    STA   $2BF2,Y
2838-   C8          INY
2839-   C0 31       CPY   #$31
283B-   D0 F5       BNE   $2832

; MLI command $80 (raw block read)
; with parameter block at $295E
283D-   20 00 BF    JSR   $BF00
2840-  [80]
2841-  [5E 29]

*295E.

295E- .. .. .. .. .. .. 03 50
2960- 00 02 00 00

So we're reading block 0 into $0200.
(The slot/drive at $295F was self-
modified earlier.)

Continuing from $2843...

; check if a page 3 vector has been
; modified (not sure what would cause
; this)
2843-   AD F9 03    LDA   $03F9
2846-   CD C8 2B    CMP   $2BC8

; if modified, fail immediately
2849-   D0 2B       BNE   $2876

*2876L

2876-   20 58 FC    JSR   $FC58
2879-   A9 0C       LDA   #$0C
287B-   85 25       STA   $25
287D-   20 22 FC    JSR   $FC22
2880-   A9 03       LDA   #$03
2882-   85 24       STA   $24
2884-   A0 00       LDY   #$00
2886-   B9 CA 2B    LDA   $2BCA,Y
2889-   C9 00       CMP   #$00
288B-   F0 06       BEQ   $2893
288D-   20 ED FD    JSR   $FDED
2890-   C8          INY
2891-   D0 F3       BNE   $2886

                 --v--

"THIS IS THE INCORRECT PROGRAM DISK"

                 --^--

Well yes, but actually no.

Continuing from $284B...

; restore page 3 vectors
284B-   A0 00       LDY   #$00
284D-   B9 F2 2B    LDA   $2BF2,Y
2850-   99 D0 03    STA   $03D0,Y
2853-   C8          INY
2854-   C0 31       CPY   #$31
2856-   D0 F5       BNE   $284D

; check for IIgs
2858-   38          SEC
2859-   20 1F FE    JSR   $FE1F

; IIgs branches
285C-   90 3E       BCC   $289C

; further checks for different 8-bit
; models -- see Tech Note 7 "Apple II
; Family Identification"
285E-   AD B3 FB    LDA   $FBB3
2861-   C9 06       CMP   #$06

; Apple ][, ][+, and /// will branch
2863-   D0 31       BNE   $2896
2865-   AD C0 FB    LDA   $FBC0
2868-   C9 00       CMP   #$00

; Apple //e, //e enhanced will branch
286A-   D0 2A       BNE   $2896
286C-   AD BF FB    LDA   $FBBF
286F-   C9 05       CMP   #$05

; Apple //c will branch
2871-   D0 23       BNE   $2896

; Apple //c+ is pretty much the only
; model left at this point
2873-   4C 22 29    JMP   $2922

So, three paths:

IIgs                ->    $289C
][, ][+, //e, ///   ->    $2896
//c+                ->    $2922

Since I'm on a //e, I'll focus on that
path.

*2896L

2896-   4C B4 28    JMP   $28B4

*28B4L

; call a routine that takes parameters
; on the stack, branch to $28D5 if it
; fails (more on this in a moment)
28B4-   20 99 28    JSR   $2899
28B7-  [04]
28B8-  [76 29]
28BA-   B0 19       BCS   $28D5

; again, but different parameters
28BC-   20 99 28    JSR   $2899
28BF-  [04]
28C0-  [7E 29]
28C2-   B0 11       BCS   $28D5

; again
28C4-   20 99 28    JSR   $2899
28C7-  [04]
28C8-  [83 29]
28CA-   B0 09       BCS   $28D5

; again
28CC-   20 99 28    JSR   $2899
28CF-  [01]
28D0-  [57 29]

; all done
28D2-   4C 22 29    JMP   $2922

*2922L

; copy this code to lower memory
2922-   A0 10       LDY   #$10
2924-   B9 B7 2B    LDA   $2BB7,Y
2927-   99 00 20    STA   $2000,Y
292A-   88          DEY
292B-   10 F7       BPL   $2924
292D-   A0 1B       LDY   #$1B
292F-   B9 3B 29    LDA   $293B,Y
2932-   99 00 02    STA   $0200,Y
2935-   88          DEY
2936-   10 F7       BPL   $292F

; and execute it from there
2938-   4C 00 02    JMP   $0200

; wipe the decrypted protection code
293B-   A0 00       LDY   #$00
293D-   A9 00       LDA   #$00
293F-   99 00 28    STA   $2800,Y
2942-   99 00 29    STA   $2900,Y
2945-   99 00 2A    STA   $2A00,Y
2948-   99 00 2B    STA   $2B00,Y
294B-   99 00 2C    STA   $2C00,Y
294E-   C8          INY
294F-   D0 EE       BNE   $293F

; restore flags (pushed at $2C24)
2951-   28          PLP

; continue with the real program code
2952-   4C 00 20    JMP   $2000

That's the success path. But if any of
the calls to $2899 fail, we end up at
$28D5, which seems bad:

; The Badlands
28D5-   A9 00       LDA   #$00
28D7-   8D 03 29    STA   $2903
28DA-   A9 C5       LDA   #$C5
28DC-   8D 04 29    STA   $2904

; relocate to lower memory
28DF-   A0 18       LDY   #$18
28E1-   B9 ED 28    LDA   $28ED,Y
28E4-   99 00 02    STA   $0200,Y
28E7-   88          DEY
28E8-   10 F7       BPL   $28E1

; and continue there
28EA-   4C 00 02    JMP   $0200

; [executed from $0200]
; wipe all of main memory
28ED-   A0 00       LDY   #$00
28EF-   A9 08       LDA   #$08
28F1-   84 06       STY   $06
28F3-   85 07       STA   $07
28F5-   91 06       STA   ($06),Y
28F7-   C8          INY
28F8-   D0 FB       BNE   $28F5
28FA-   E6 07       INC   $07
28FC-   A5 07       LDA   $07
28FE-   C9 C0       CMP   #$C0
2900-   D0 F3       BNE   $28F5

; forever
2902-   4C 00 02    JMP   $0200

So we're doing a thing at $2899, four
times but with different parameters,
and if they all work, we clean up and
continue to the real program code.

Let's see what we're doing at $2899.

2899-   4C 03 C5    JMP   $C503

The jump address at $2899 was self-
modified earlier as $C5FF + #$03. That
would make it the entry point to the
SmartPort firmware.

So that's great.

                   ~

               Chapter 3
      In Which Everyone Is Smart
           In Their Own Way


SmartPort firmware is documented in
"Apple IIgs Firmware Reference," ch. 7.

                 --v--

This is an example of a standard
SmartPort call:

; Call SmartPort command dispatcher
SP_CALL   JSR  DISPATCH

; This specifies the command type
          DFB  CMDNUM

; word pointer to the parameter list
          DW   CMDLIST

; carry is set on an error
          BCS  ERROR

                 --^--

That's exactly what we're doing at
$28B4 -- calling the command dispatch.
The next three bytes are a command
number and the address of a parameter
block. Then we branch to The Badlands
at $28D5 on error.

28B4-   20 99 28    JSR   $2899
28B7-  [04]
28B8-  [76 29]
28BA-   B0 19       BCS   $28D5

In this first call, we're issuing the
SmartPort command #$04 with a parameter
block at $2976. This is a "control
call" -- an extension mechanism to send
commands that different devices can
interpret in different ways. The
control calls for the UniDisk 3.5 are
documented later in chapter 7.

(This, by the way, explains why the
program "requires" a UniDisk 3.5 drive.
It's not the program that requires it,
it's the copy protection.)

The parameter block at $2976 gives the
details on this "control call."

*2976.

2976- .. .. .. .. .. .. 03 01
2978- 7B 29 06 02 00 05 03

Taking this one byte at a time:

$2976: $03   parameter count
$2977: $01   unit number (possibly self-
             modified above to support
             running from drive 2)
$2978: $297B address of "control list,"
             i.e. the parameter block
             for this custom call
$297A: $06   control code: "SetAddress"
$297B: $0002 block size (always 2)
$297D: $0305 address within the UniDisk
             drive

The firmware reference manual describes
the "SetAddress" control call:

                 --v--

This call is used to set the address in
the UniDisk 3.5 controller's memory
space that the Download call will load
a 65C02 routine into. Care must be
taken that the download address is set
only to free space in the UniDisk 3.5
memory map.

                 --^--

So we're setting up an environment to
transfer executable code to the drive
itself. Because that's not completely
insane. (Yes, I'm aware that this was
common on other platforms like the
Commodore 64. That doesn't make it any
less insane.)

So what are we downloading? That's the
next call, at $28BC:

28BC-   20 99 28    JSR   $2899
28BF-  [04]
28C0-  [7E 29]
28C2-   B0 11       BCS   $28D5

Same deal, we're calling the SmartPort
firmware with a control call. $28BF is
#$04, a control call command. $28C0 is
$297E, the address of the parameter
block. And we branch to The Badlands on
error.

Looking at the parameter block:

*297E.

297E- .. .. .. .. .. .. 03 01
2980- 00 2A 07

We see a similar structure as the first
call, but with a different control code
(at $2982):

$297E: $03   parameter count
$297F: $01   unit number (possibly self-
             modified above to support
             running from drive 2)
$2980: $2A00 address of parameter block
$2982: $07   control code: "Download"

This is what the firmware reference
manual has to say about the "Download"
call:

                 --v--

This call is used to download an
executable 65C02 routine into the
memory resident on the UniDisk 3.5
controller. The address that the
routine is loaded into is set by the
SetAddress call. The count field must
be set to the length of the 65C02
routine to be downloaded.

                 --^--

So $2980 points to $2A00, which will
contain a length word followed by the
actual code to download to the drive.
The code is 65c02 code, so I can use
the monitor disassembly to read it.

*2A00.

2A00- 70 00

#$70 bytes of code, starting at $2A02.
But it will be executed on the drive
itself, at address $0305.

We'll look at it in a moment. First,
for completeness, I want to note that
this "Download" control call does not
auto-execute the code it transfers to
the drive. That happens in the third
call, at 28C4:

28C4-   20 99 28    JSR   $2899
28C7-  [04]
28C8-  [83 29]
28CA-   B0 09       BCS   $28D5

$28C7 is #$04, so we are once again
doing a control call. $28C8 points to
the parameter block at 2983. We jump
to The Badlands if there's any error.

*2983.

2983- .. .. .. 03 01 88 29 05

$2983: $03   parameter count
$2984: $01   unit number (possibly self-
             modified above to support
             running from drive 2)
$2985: $2988 address of parameter block
$2987: $05   control code: "Execute"

Again from the fine manual:

                 --v--

This call is used to dispatch the
intelligent controller in the UniDisk
3.5 device to execute a 65C02
subroutine. The register setup is
passed to the routine to be executed
from the control list.

                 --^--

The parameter block at $2988 gives the
initial values of each register. (This
is a 65c02, so it has A, X, and Y
registers just like the Apple II its
connected to.)


*2988.

2988- 06 00 05 05 34 00 05 03

$2988: $0006 block size (always 6)
$298A: $05   A value
$298B: $05   X value
$298C: $34   Y value
$298D: $00   flags value (like PHP/PLP)
$298E: $0305 address of code to execute

Unsurprisingly, we are executing the
code we just downloaded to $0305.

Now let's look at that code.

                   ~

               Chapter 4
   In Which Murphy's Law Never Fails


Taking advantage of the fact that this
floppy drive runs the same processor as
the host Apple II (I AM STILL NOT OVER
THAT, BY THE WAY), we can manually move
the code to $0305 on the Apple II and
use the monitor disassembly to list it.

*305<2A02.2A71M

*305L

; save some zero page addresses
0305-   A5 73       LDA   $73
0307-   8D 47 05    STA   $0547
030A-   A5 74       LDA   $74
030C-   8D 48 05    STA   $0548

; copy some zero page values
030F-   A0 05       LDY   #$05
0311-   B1 73       LDA   ($73),Y
0313-   99 20 05    STA   $0520,Y
0316-   88          DEY
0317-   D0 F8       BNE   $0311

; overwrite some zero page addresses
0319-   A9 4C       LDA   #$4C
031B-   85 75       STA   $75
031D-   A9 20       LDA   #$20
031F-   85 73       STA   $73
0321-   A9 05       LDA   #$05
0323-   85 74       STA   $74

; call... something in ROM
0325-   20 6A E5    JSR   $E56A
0328-   20 62 E1    JSR   $E162
032B-   EA          NOP

; get a raw nibble from the disk (like
; "LDA $C08C,X" on a 5.25-inch floppy)
032C-   AD 0E 0A    LDA   $0A0E
032F-   10 FB       BPL   $032C

; loop until we find a $D5 nibble
0331-   C9 D5       CMP   #$D5
0333-   D0 F7       BNE   $032C

; burn CPU cycles
0335-   48          PHA
0336-   68          PLA

; next nibble must be $B5 (nonstandard)
0337-   AD 0E 0A    LDA   $0A0E
033A-   10 FB       BPL   $0337
033C-   C9 B5       CMP   #$B5

; otherwise loop back to find another
; $D5 nibble
033E-   D0 EC       BNE   $032C

; restore RdAddr hooks in zero page
0340-   AD 47 05    LDA   $0547
0343-   85 73       STA   $73
0345-   AD 48 05    LDA   $0548
0348-   85 74       STA   $74
034A-   38          SEC
034B-   60          RTS

According to the fine manual, zero page
$73 and $74 are part of a "hook table"
that jumps to all the hookable routines
the drive supports. This includes
routines like "RdAddr: find and decode
an address field" ($73/$74), "ReadData:
find and load a data field in RAM"
($76/$77), and so on.

So we're setting the "RdAddr" hook to
point to $0520. Well, we just copied 5
bytes of code from ($73) to $0521 (at
$030F), but nothing to address $0520.
The fine manual lists $0500..$05FF as
"free space," so I'm confused. What
exactly is supposed to be at $0520?

The answer is... a bit shocking. This
entire copy protection routine was set
up incorrectly. We're not supposed to
be downloading code to the address
$0305 inside the floppy drive. No,
really, we're not supposed to do that.
The fine manual says that's part of a
buffer for "host communication (format
sector buffer I)." I'm not sure what
that is, but it is definitely not "free
space." The $0500..$05FF page is listed
listed as "free space." $0305 is not.

The root cause, I believe, is an off-
by-1 bug in the parameter blocks of the
original control calls. This code isn't
supposed to be downloaded and executed
at $0305; it's supposed to be at $0500.
That makes more sense on its face, just
because that's the largest block of
free space in the drive's memory map.
But also, it would make the code itself
make more sense.

Observe.

Suppose, for the sake of argument, that
this code ended up at $0500 instead of
$0305. Then it would look like this:

0500-   A5 73       LDA   $73
0502-   8D 4C 05    STA   $054C
0505-   A5 74       LDA   $74
0507-   8D 4D 05    STA   $054D
050A-   A0 05       LDY   #$05
050C-   B1 73       LDA   ($73),Y
050E-   99 20 05    STA   $0520,Y
0511-   88          DEY
0512-   D0 F8       BNE   $050C
0514-   A9 4C       LDA   #$4C
0516-   85 75       STA   $75
0518-   A9 20       LDA   #$20
051A-   85 73       STA   $73
051C-   A9 05       LDA   #$05
051E-   85 74       STA   $74

; now these two JSR calls are self-
; modified by the code above
0520-   20 6A E5    JSR   $E56A
0523-   20 62 E1    JSR   $E162
0526-   EA          NOP
0527-   AD 0E 0A    LDA   $0A0E
052A-   10 FB       BPL   $0527
052C-   C9 D5       CMP   #$D5
052E-   D0 F7       BNE   $0527
0530-   48          PHA
0531-   68          PLA
0532-   AD 0E 0A    LDA   $0A0E
0535-   10 FB       BPL   $0532
0537-   C9 B5       CMP   #$B5
0539-   D0 EC       BNE   $0527
053B-   AD 4C 05    LDA   $054C
053E-   85 73       STA   $73
0540-   AD 4D 05    LDA   $054D
0543-   85 74       STA   $74
0545-   38          SEC
0546-   60          RTS

If this had been downloaded to, and
executed from, address $0500, the
RdAddr hook at $73/$74 would have been
redirected to $0520. The code at $520
would have been self-modified to be the
first 5 bytes of code from the original
RdAddr routine, followed by the custom
code starting at $0527. That custom
code would check for a nonstandard
nibble sequence on the next disk read,
looping forever if it couldn't find it.
(The drive has a "watchdog" timeout, so
this would eventually just time out and
return an error to the host Apple II.)
If it succeeded in finding the custom
nibble sequence, it would have restored
the RdAddr hook at $73/$74 before
returning.

All that, combined with the final
SmartPort call at $28CC...

28CC-   20 99 28    JSR   $2899
28CF-  [01]
28D0-  [57 29]

Command #$01 is a "read" command. So we
would have the drive read a block from
the disk, but with the hooked RdAddr
routine that points to the protection
code at $0520.

*2957.

2957- .. .. .. .. .. .. .. 03
2958- 01 00 02 10 00 00

$2957: $03   parameter count
$2958: $01   unit number (possibly self-
             modified above to support
             running from drive 2)
$2959: $0200 address of read buffer (in
             the drive memory)
$295B: $000010 block number

And *that* SmartPort call would succeed
or fail based on whether it found the
custom nibble sequence ($D5 $B5) while
attempting to read block $10.

And *that* is the entire protection: a
single custom nibble near block $10.

Except none of that happened, because
there was an off-by-1 bug in the
parameter block of the control call, so
the code that self-modified as if were
at $0500 ended up being at $0305
instead, and I HAVE LITERALLY NO IDEA
HOW THIS EVER WORKED AT ALL, even on an
original disk with a compatible UniDisk
3.5 drive.

The RdAddr hook ($73/$74) ends up
pointing to uninitialized memory at
$0520, but because the JSRs at $0325
are never self-modified, it falls
through to the actual protection code
to check for a custom nibble sequence,
but during the Execute call instead of
the final ReadBlock call, then restores
the RdAddr hook before returning. So
the ReadBlock call will always succeed,
because by the time it happens, the
RdAddr hook has been restored to its
original value.

I think.

I have seen variants of this protection
on two different disks, and in both
cases, the //e code, that downloads
executable code to the drive, had this
off-by-1 bug. Presumably the IIgs code
(which I did not show) actually fails
properly on an unauthorized copy, and
that's primarily what the developers
tested.

At any rate, copy protection barely
works in the best of circumstances,
which these are not. Downloading code
to peripherals is insane in the best of
circumstances, which these are not. I
will die on this hill. (*)

(*) not guaranteed, actual death may
    vary

The protection routine has no side
effects. We can bypass the entire thing
by changing the JMP at $2C6D to jump
directly to the success path at $2922.

Using Glen Bredon's "Block Warden," I
can search the disk for the offending
JSR. The syntax always confuses me, so
for future me, the correct sequence is

  [C]hange device to "Slot 7, Drive 2"
  [E]dit
  [Ctrl-S] and enter "$6C712C" as the
           search string to search for
           a hex sequence (this is the
           indirect JMP at $2C6D)

It finds the code on block $003A.

; instead of entering the copy
; protection routine, jump directly to
; the success path
$003A,$6D: 6C 71 2C -> 4C 22 29

As an added bonus, we're removed the
only thing that tied us to the UniDisk
3.5, so I can boot my cracked copy on
my Apple SuperDrive. (I tried copying
it to a subdirectory on my ProDOS hard
drive, but it fails with a "Directory
not found" error. A project for another
day.)

Anyway, lovely game. Shame about the
protection.

Quod erat liberandum.

---------------------------------------
A 4am crack                    No. 2142
------------------EOF------------------
